package com.joshua.spring.aspect;

import com.joshua.spring.annotation.AroundLogger;
import com.joshua.spring.constants.AspectOrderConstants;
import com.joshua.spring.util.IpAddressUtils;
import com.joshua.spring.util.JacksonUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Stream;

import static com.joshua.spring.constants.FormatterConstants.*;
import static com.joshua.spring.constants.LogInfoConstants.*;
import static com.joshua.spring.constants.SymbolConstants.EMPTY_STR;
import static com.joshua.spring.constants.SymbolConstants.LEFT_SLASH;

/**
 * description
 * package com.joshua.spring.aspect
 * classname  MicroServiceRequestLoggerAspect
 *
 * @author <a href="mailto:joshualwork@163.com">joshua_liu</a>
 * @date 2020/4/3 11:08
 */
@Slf4j
@Aspect
@Component
@Order(AspectOrderConstants.PRIORITY_RANK)
public class MicroServiceRequestLoggerAspect implements InitializingBean {

    private static final AntPathMatcher pathMatcher = new AntPathMatcher();
    private static final String DEFAULT_ENV = "dev";

    @Value("${spring.profiles.active:test}")
    private String env;

    @Pointcut("@annotation(com.joshua.spring.annotation.AroundLogger)")
    private void allRequestMethod() {
    }

    @Before("allRequestMethod()")
    public void doBefore(JoinPoint joinPoint) {
        AroundLogger aroundLogger = getAroundLogger(joinPoint);
        Set<AroundLogger.AroundType> aroundTypes = defaultAroundTypeSet();
        aroundTypes.add(AroundLogger.AroundType.BEFORE);
        if (loggerCondition(aroundLogger, aroundTypes)) {
            log.info("\n\tTrace info start,trace info[t:{} |s:{} |ps:{}]",
                    MDC.get(X_TRACE_ID), MDC.get(X_SPAN_ID), MDC.get(X_P_SPAN_ID));
        }
    }

    @Around("allRequestMethod()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
//        Object[] args = proceedingJoinPoint.getArgs();
//        List<Object> list = Arrays.asList(args);
//        list.parallelStream().filter(e -> e instanceof FormInter).peek(e -> {
//            FormInter formInter = (FormInter) e;
//            formInter.checkParams();
//        });
        Object result = proceedingJoinPoint.proceed();
        AroundLogger aroundLogger = getAroundLogger(proceedingJoinPoint);
        Set<AroundLogger.AroundType> aroundTypes = defaultAroundTypeSet();
        aroundTypes.add(AroundLogger.AroundType.AROUND);
        if (loggerCondition(aroundLogger, aroundTypes)) {
            MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
            Method method = methodSignature.getMethod();
            String path = getMethodPath(request.getRequestURI(), request.getContextPath(), method);
            long timeCost = System.currentTimeMillis() - start;
            StringBuffer logBuffer = new StringBuffer("\n\tHttp request client ip [{}], method [{}], cost time [{}] ms");
            logBuffer = logBufferFormatter(logBuffer);
            logBuffer.append("\n\tresponse data [{}]");
            Map<String, Object> params = getRequestParams(proceedingJoinPoint);
            params.remove("request");
            String paramJson = CollectionUtils.isEmpty(params) ? EMPTY_STR : JacksonUtils.obj2json(params);
            log.info(logBuffer.toString(), IpAddressUtils.getIpAddress(request), request.getMethod(),
                    timeCost, request.getRequestURL(), request.getRequestURI(), path,
                    paramJson, method, result);
        }
        return result;
    }

    @After("allRequestMethod()")
    public void doAfter(JoinPoint joinPoint) {
        AroundLogger aroundLogger = getAroundLogger(joinPoint);
        Set<AroundLogger.AroundType> aroundTypes = defaultAroundTypeSet();
        aroundTypes.add(AroundLogger.AroundType.AFTER);
        if (loggerCondition(aroundLogger, aroundTypes)) {
            log.info("\n\tTrace info end,trace info[t:{} |s:{} |ps:{}]",
                    MDC.get(X_TRACE_ID), MDC.get(X_SPAN_ID), MDC.get(X_P_SPAN_ID));
        }
    }

    @AfterThrowing(pointcut = "allRequestMethod()", throwing = "e")
    public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String path = getMethodPath(request.getRequestURI(), request.getContextPath(), method);
        StringBuffer logBuffer = new StringBuffer("\n\tHttp request client ip [{}], method [{}]");
        logBuffer = logBufferFormatter(logBuffer);
        logBuffer.append("\n\tcatch exception:[{}]");
        Map<String, Object> params = getRequestParams(joinPoint);
        String paramJson = CollectionUtils.isEmpty(params) ? EMPTY_STR : JacksonUtils.obj2json(params);
        log.info(logBuffer.toString(), IpAddressUtils.getIpAddress(request), request.getMethod(),
                request.getRequestURL(), request.getRequestURI(), path,
                paramJson, method, e);
    }

    private AroundLogger getAroundLogger(JoinPoint point) {
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        AroundLogger result = method.getDeclaringClass().getAnnotation(AroundLogger.class);
        return result;
    }

    private Set<AroundLogger.AroundType> defaultAroundTypeSet() {
        Set<AroundLogger.AroundType> aroundTypes = new HashSet<>();
        aroundTypes.add(AroundLogger.AroundType.ALL);
        return aroundTypes;
    }

    private boolean loggerCondition(AroundLogger aroundLogger, Set<AroundLogger.AroundType> aroundTypes) {
        List<String> envList = Arrays.asList(aroundLogger.env());
        return envList.contains(env) && (aroundTypes.contains(aroundLogger.value()));
    }

    private StringBuffer logBufferFormatter(StringBuffer logBuffer) {
        logBuffer.append("\n\turl [{}]");
        logBuffer.append("\n\turi [{}]");
        logBuffer.append("\n\tpath [{}]");
        logBuffer.append("\n\tparams [{}]");
        logBuffer.append("\n\tclass method [{}]");
        return logBuffer;
    }

    private Map<String, Object> getRequestParams(ProceedingJoinPoint proceedingJoinPoint) {
        //参数名
        String[] paramNames = ((MethodSignature) proceedingJoinPoint.getSignature()).getParameterNames();
        //参数值
        Object[] paramValues = proceedingJoinPoint.getArgs();

        return buildRequestParam(paramNames, paramValues);
    }

    private Map<String, Object> getRequestParams(JoinPoint joinPoint) {
        //参数名
        String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        //参数值
        Object[] paramValues = joinPoint.getArgs();
        return buildRequestParam(paramNames, paramValues);
    }

    private Map<String, Object> buildRequestParam(String[] paramNames, Object[] paramValues) {
        Map<String, Object> requestParams = new HashMap<>(32);
        for (int i = 0; i < paramNames.length; i++) {
            Object value = paramValues[i];
            //如果是文件对象
            if (value instanceof MultipartFile) {
                MultipartFile file = (MultipartFile) value;
                //获取文件名
                value = file.getOriginalFilename();
            }
            requestParams.put(paramNames[i], value);
        }
        return requestParams;
    }

    private String getMethodPath(String uri, String contextPath, Method method) {
        RequestMapping clazzBaseMapping = method.getDeclaringClass().getAnnotation(RequestMapping.class);
        String[] clazzBaseValues = clazzBaseMapping.value();
        String[] values;
        RequestMapping methodRequestMapping = method.getAnnotation(RequestMapping.class);
        GetMapping methodGetMapping = method.getAnnotation(GetMapping.class);
        PostMapping methodPostMapping = method.getAnnotation(PostMapping.class);
        DeleteMapping methodDelMapping = method.getAnnotation(DeleteMapping.class);
        PutMapping methodPutMapping = method.getAnnotation(PutMapping.class);
        PatchMapping methodPatchMapping = method.getAnnotation(PatchMapping.class);
        if (!ObjectUtils.isEmpty(methodRequestMapping)) {
            values = methodRequestMapping.value();
        } else if (!ObjectUtils.isEmpty(methodGetMapping)) {
            values = methodGetMapping.value();
        } else if (!ObjectUtils.isEmpty(methodPostMapping)) {
            values = methodPostMapping.value();
        } else if (!ObjectUtils.isEmpty(methodPutMapping)) {
            values = methodPutMapping.value();
        } else if (!ObjectUtils.isEmpty(methodPatchMapping)) {
            values = methodPatchMapping.value();
        } else if (!ObjectUtils.isEmpty(methodDelMapping)) {
            values = methodDelMapping.value();
        } else {
            values = new String[]{EMPTY_STR};
        }
        String clazzMapping = mappingInfoEmptyJudge(clazzBaseValues[0]);
        String methodMapping = EMPTY_STR;
        if (values.length > 1) {
            String finalContextPath = contextPath;
            Optional<String> resultWholePath = Stream.of(values).map(e -> {
                e = mappingInfoEmptyJudge(e);
                String wholePath = String.format(FORMATTER3S, finalContextPath, clazzMapping, e);
                if (pathMatcher.match(wholePath, uri)) {
                    return wholePath;
                } else {
                    return EMPTY_STR;
                }
            }).filter(e -> !StringUtils.isEmpty(e)).findFirst();
            if (resultWholePath.isPresent()) {
                return resultWholePath.get();
            }
        } else {
            String methodValue = ObjectUtils.isEmpty(values) ? LEFT_SLASH : values[0];
            methodMapping = mappingInfoEmptyJudge(methodValue);
        }
        contextPath = StringUtils.isEmpty(contextPath) ? EMPTY_STR : contextPath;
        return String.format(FORMATTER3S, contextPath, clazzMapping, methodMapping);
    }

    private String mappingInfoEmptyJudge(String mappingInfo) {
        if (!StringUtils.isEmpty(mappingInfo)) {
            if (!mappingInfo.startsWith(LEFT_SLASH)) {
                mappingInfo = String.format(PATH_FORMATTER, mappingInfo);
            }
        } else {
            mappingInfo = EMPTY_STR;
        }
        return mappingInfo;
    }

    @Override
    public void afterPropertiesSet() throws Exception {

    }


}
